package autotd;



import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class MonsterGenerator {
	/** This class represents a sequence of Monsters rather than a single monster.
	*/
	private static class GeneratorEntry {
		public final MonsterType MType;
		public final int MLevel;
		public final int FirstTime; // First time that we spawn.
		public final int TimePeriod; // Number of turns between each respawn.
		public final int NumberOfTimes; // Total number of repeats, -1 => infinite repeats
		public GeneratorEntry(MonsterType mtype, int mlevel, int firstTime, int timePeriod, int numberOfTimes) {
			MType = mtype;
			MLevel = mlevel;
			FirstTime = firstTime;
			TimePeriod = timePeriod;
			NumberOfTimes = numberOfTimes;
		}
		// Constructor for creating a new, tougher entry.
		public GeneratorEntry(GeneratorEntry oldGE, int firstTimeDelta, int levelDelta) {
			MType = oldGE.MType;
			MLevel = oldGE.MLevel + levelDelta;
			FirstTime = oldGE.FirstTime + firstTimeDelta;
			TimePeriod = oldGE.TimePeriod;
			NumberOfTimes = oldGE.NumberOfTimes;
		}
	}

	/** Stores the original entries for the purposes of generating the repeating waves. */
	private List<GeneratorEntry> myRawEntries;

	/** The period at which the original schedule is repeated with higher level monsters. */
	private int myRepeatSchedulePeriod;

	/** The level boost the monsters get each time the whole schedule is repeated. */
	private int myRepeatScheduleLevelBoost;

	/** The next time each monster will appear. */
	private Map<Integer,List<GeneratorEntry>> mySchedule;

	/** The GameMap, necessary for creating new Monsters. */
	private GameMap myGameMap;

	/** The current number of ticks we have seen so far. */
	private int myCurrentTime;


	/** Default constructor.
	* @param gm The GameMap this generator will be making monsters for.
	*/	
	public MonsterGenerator(GameMap gm) {
		myRawEntries = new ArrayList<GeneratorEntry>();
		myRepeatSchedulePeriod = Integer.MAX_VALUE;
		myRepeatScheduleLevelBoost = 0;
		mySchedule = new HashMap<Integer, List<GeneratorEntry>>();
		myGameMap = gm;
		myCurrentTime = 0;
	}


	/** Add a new entry for monster generation.
	* @param ge The new entry for generating monsters.
	* @return void
	*/
	private void addEntry(GeneratorEntry ge) {
		myRawEntries.add(ge);
		schedule(ge, nextAppearance(ge));
	}

	/** Calculates the next time a monster will appear.
	* @param ge The monster schedule to find the next appearance of.
	* @return int The next time that monster will appear, or -1 if it is completely exhausted.
	*/
	private int nextAppearance(GeneratorEntry ge) {
		// BEWARE!!! CHANGE THE SCHEDULING SYSTEM AT YOUR OWN RISK!
		// Remember that -1 and zeros can appear in various places.
		if (ge.FirstTime > myCurrentTime) {
			return ge.FirstTime;
		} else if (ge.TimePeriod == 0) {
			return -1;
		} else {
			int gap = (myCurrentTime + 1 - ge.FirstTime) / ge.TimePeriod;
			int insertionTime = ge.FirstTime + gap * ge.TimePeriod;
			if (insertionTime <= myCurrentTime) {
				gap++;
				insertionTime += ge.TimePeriod;
			}
			if ((ge.NumberOfTimes == -1) || (gap < ge.NumberOfTimes)) {
				return insertionTime;
			}
		}
		return -1;
	}

	/** Adds an entry to the schedule.
	* @param ge The monster to add to the schedule.
	* @param time The time the monster will next appear.
	* @return boolean Whether or not the scheduling was successful.
	*/
	private boolean schedule(GeneratorEntry ge, int time) {
		if (time <= myCurrentTime) {
			return false;
		}
		if (!mySchedule.containsKey(time)) {
			mySchedule.put(time, new ArrayList<GeneratorEntry>());
		}
		mySchedule.get(time).add(ge);
		return true;
	}


	/** Checks to see if there are any more monsters left to defeat.
	* @return boolean Is the schedule completely exhausted?
	*/
	public boolean areAllMonstersFinished() {
		return mySchedule.isEmpty();
	}

	
	/** Increment the current time, then return the monsters which spawn at this time.
	* @return Monster[] The monsters which spawn at the beginning of the next tick.
	*/
	public Monster[] tick() {
		myCurrentTime++;
		
		// Check for total respawn in the next turn.
		if ((myCurrentTime + 1) % myRepeatSchedulePeriod == 0) {
			List<GeneratorEntry> oldEntries = myRawEntries;
			myRawEntries = new ArrayList<GeneratorEntry>();
			for (GeneratorEntry ge : oldEntries) {
				addEntry(new GeneratorEntry(ge, myRepeatSchedulePeriod + 1, myRepeatScheduleLevelBoost));
			}
			System.out.println("REGENERATION " + myRawEntries.size());
		}

		// Check to see if there are no monsters for this tick.
		if (!mySchedule.containsKey(myCurrentTime)) {
			return new Monster[0];
		}

		List<GeneratorEntry> entries = mySchedule.remove(myCurrentTime);

		// Create the spawned monsters.
		Monster[] result = new Monster[entries.size()];
		for (int pos = 0; pos < entries.size(); pos++) {
			result[pos] = new Monster(entries.get(pos).MType, entries.get(pos).MLevel);
		}

		// Reschedule the entries with repeats left.
		for (int pos = 0; pos < entries.size(); pos++) {
			schedule(entries.get(pos), nextAppearance(entries.get(pos)));
		}
		
		return result;
	}


	/** Method to determine the time when the next monster will be created (after now).
	* @return The absolute time when the next monster will appear after now.
	* @throws IllegalStateException If there are no more monsters left in the scheulde.
	*/
	public int nextMonsterTime() throws IllegalStateException {
		if (areAllMonstersFinished()) {
			throw new IllegalStateException();
		}
		int nextTime = Integer.MAX_VALUE;
		for (Integer entryTime : mySchedule.keySet()) {
			nextTime = Math.min(nextTime, entryTime);
		}
		return nextTime;
	}


	/** Method to determine the number of additional ticks until the next monster will be created.
	* @return The absolute time when the next monster will appear after now.
	* @throws IllegalStateException If there are no more monsters left in the scheulde.
	*/	
	public int timeToNextMonster() throws IllegalStateException{
		return nextMonsterTime() - myCurrentTime;
	}






	public static MonsterGenerator parseFile(GameMap gm, List<MonsterType> types, File f) throws FileNotFoundException, IOException {
		MonsterGenerator mg = new MonsterGenerator(gm);
		BufferedReader input = new BufferedReader(new FileReader(f));
		String line = input.readLine();
		int baseTime = 0;
		Map<String, MonsterType> mtypes;
		mtypes = new HashMap<String, MonsterType>();
		for (MonsterType mt : types) {
			mtypes.put(mt.Name, mt);
		}
		while (line != null) {
			if (line.indexOf("#") != -1) {
				line = line.substring(0, line.indexOf("#"));
			}
			String[] tokens = line.split(" ");
			if (tokens.length > 0) {
				if (tokens[0].equals("skip")) {
					baseTime += Integer.parseInt(tokens[1]);
				} else if (tokens[0].equals("repeat")) {
					mg.myRepeatSchedulePeriod = Integer.parseInt(tokens[1]);
					mg.myRepeatScheduleLevelBoost = Integer.parseInt(tokens[2]);
				} else if (tokens[0].equals("spawn")) {
					String typeName = tokens[1];
					// ignore any monsters of unknown type
					if (mtypes.containsKey(typeName)) {
						GeneratorEntry ge = new GeneratorEntry(
							mtypes.get(typeName),
							Integer.parseInt(tokens[2]),
							baseTime + Integer.parseInt(tokens[3]),
							Integer.parseInt(tokens[4]),
							Integer.parseInt(tokens[5])
						);
						mg.addEntry(ge);
					}
				}
			}
			line = input.readLine();
		}
		return mg;
	}
}









